package com.cirnoworks.uhs.server;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;

import javax.inject.Singleton;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cirnoworks.common.ByteBufferUtil;
import com.cirnoworks.uhs.Link;
import com.cirnoworks.uhs.Protocal;

@Singleton
public class ControlLink extends Link {

	private final HashMap<String, Session> sessions = new HashMap<String, Session>();
	private final ServerContainer server;
	private long lastSessionCheck;
	private final ThreadLocal<UserKey> key = new ThreadLocal<UserKey>() {

		@Override
		protected UserKey initialValue() {
			return new UserKey();
		}

	};

	private static final Logger LOG = LoggerFactory
			.getLogger(ControlLink.class);

	public ControlLink(ServerContainer server, Selector selector, int port)
			throws IOException {
		super(selector, server, port);
		this.server = server;
	}

	@Override
	public void onClose() {

	}

	@Override
	protected void receiveData(Selector selector, SocketAddress from,
			ByteBuffer buf) {
		if (buf.remaining() < 9) {
			// discard invalid package
			return;
		}
		final int op = buf.get();
		final long seq = buf.getLong();
		switch (op) {
		case Protocal.CmdPing.OP:
			ping(selector, from, buf);
			break;
		case Protocal.CmdPortMap.OP:
			portmap(selector, from, seq, key.get(), buf);
			break;
		case Protocal.CmdGetPortMap.OP:
			getPort(selector, from, seq, key.get(), buf);
			break;
		case Protocal.RedirectPayload.OP:
			buf.position(1);
			redirect(selector, from, key.get(), buf);
			break;
		default:
			LOG.warn("Illegal op=" + op);
		}
	}

	public void sessionCheck() {
		long now = System.currentTimeMillis();
		if (now - lastSessionCheck > 300000) {
			lastSessionCheck = now;

			int portmapRemoved = 0;
			int sessionRemoved = 0;
			int portmapCount = 0;
			LOG.info("Removing unused sessions");
			Iterator<Entry<String, Session>> si = sessions.entrySet()
					.iterator();
			while (si.hasNext()) {
				Entry<String, Session> entry = si.next();
				Session session = entry.getValue();
				HashMap<Character, PortMap> portmaps = session.getPortmaps();
				Iterator<Entry<Character, PortMap>> pi = portmaps.entrySet()
						.iterator();
				while (pi.hasNext()) {
					Entry<Character, PortMap> portmap = pi.next();
					if (now - portmap.getValue().getLastUpdateTime() > 180000) {
						portmap.getValue().close();
						pi.remove();
						portmapRemoved++;
					}
				}
				if (portmaps.size() == 0 && session.isTimeout()) {
					si.remove();
					sessionRemoved++;
				} else {
					portmapCount += portmaps.size();
				}
			}
			LOG.info("Removing unused sessions..." + portmapRemoved + "/"
					+ portmapCount + " portmaps removes, " + sessionRemoved
					+ "/" + sessions.size() + " sessions removed.");
		}
	}

	private void ping(Selector selector, SocketAddress from, ByteBuffer buf) {
		try {
			buf.position(9);
			buf.flip();
			sendData(selector, from, buf);
		} catch (Exception e) {
			LOG.error("Error in op ping", e);
		}
	}

	private void portmap(Selector selector, SocketAddress from, long seq,
			UserKey key, ByteBuffer buffer) {
		try {
			if (buffer.remaining() < 36) {
				return;
			}
			key.pushHash(buffer);
			int port = buffer.getChar();
			int redirectorPort = buffer.getChar();

			User user = server.getUser(key);
			if (user == null) {
				return;
			}
			Session session = sessions.get(user.getUserName());
			if (session == null) {
				session = new Session(key);
				session.setUserName(user.getUserName());
				LOG.info("Session created for user " + user.getUserName());
				sessions.put(user.getUserName(), session);
			}

			PortMap portMap = session.getPortmaps().get((char) port);
			if (portMap == null) {
				portMap = new PortMap(server, this, selector);
				LOG.info("Portmap created for user " + session.getUserName()
						+ " port " + port);
				session.getPortmaps().put((char) port, portMap);
			}

			// if (!from.equals(portMap.getNetworkAddress())) {
			// LOG.info("Update portmap target to " + from);
			// }

			portMap.setLastUpdateTime(System.currentTimeMillis());
			portMap.setNetworkAddress(from);
			portMap.setServicePort(port);
			portMap.setRedirectorPort(redirectorPort);
			portMap.updateSublinkWhenNecessary();

			buffer.clear();
			Protocal.CmdPortMap.response(buffer, seq, 0);
			buffer.flip();
			sendData(selector, from, buffer);
		} catch (Exception e) {
			LOG.error("Error in op portmap", e);
		}
	}

	private void getPort(Selector selector, SocketAddress from, long seq,
			UserKey key, ByteBuffer buffer) {
		try {
			String userName = ByteBufferUtil.getString(buffer);
			char port = buffer.getChar();
			Session session = sessions.get(userName);
			if (session == null) {
				return;
			}
			PortMap pm = session.getPortmaps().get(port);
			if (pm == null) {
				return;
			}

			buffer.clear();
			Protocal.CmdGetPortMap
					.response(buffer, seq, pm.getNetworkAddress());
			buffer.flip();
			sendData(selector, from, buffer);
		} catch (Exception e) {
			LOG.error("Error in op getPort", e);
		}
	}

	private void redirect(Selector selector, SocketAddress from, UserKey key,
			ByteBuffer payload) {
		try {
			key.pushHash(payload);
			char servicePort = payload.getChar();
			User user = server.getUser(key);
			if (user == null) {
				LOG.warn("Illegal user");
			}
			Session session = sessions.get(user.getUserName());
			if (session == null) {
				LOG.warn("Illegal session");
			}
			PortMap pm = session.getPortmaps().get(servicePort);
			if (pm == null) {
				LOG.warn("Illegal port map");
			}
			pm.getSublink().sendData(selector, payload);
		} catch (Exception e) {
			LOG.error("Error in op redirect", e);
		}
	}

}
